﻿#region License
// Copyright 2010 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.IO;
using MediaWiki.Common;
using MediaWiki.MediaWikiConverter.Utilities.Logging;
using MediaWiki.Office.Model;
using MediaWiki.Office.Packaging;
using System.Text.RegularExpressions;
using Image=MediaWiki.Office.Model.Image;

namespace MediaWiki.MediaWikiConverter.Exporter
{
  public class MediaWikiExporter
  {
    private readonly Stream _source;
    private readonly Stream _output;

    private readonly StringBuilder _builder;
    private readonly List<State> _state;

    private static readonly Regex _urlRegex = new Regex(
      @"(?#Protocol)(?:(?:ht|f)tp(?:s?)\:\/\/|~\/|\/)?(?#Username:Password)(?:\w+:\w+@)?(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:\/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|\/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=?(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)?",
      RegexOptions.Compiled);

    public Encoding OutputEncoding { get; set; }

    private Document _document;

    public MediaWikiExporter(Stream source, Stream output)
    {
      _source = source;
      _output = output;
      _builder = new StringBuilder();
      _state = new List<State>();

      OutputEncoding = Encoding.UTF8;
    }

    private StateContext StateContext(State state)
    {
      return new StateContext(_state, state);
    }

    private bool IsHeading(string style, out int level)
    {
      if (!string.IsNullOrEmpty(style))
      {
        Match match = Regex.Match(style, "Heading(?<Level>[0-9])", RegexOptions.CultureInvariant | RegexOptions.Compiled);
        if (match != null && match.Success)
        {
          Group levelGroup = match.Groups["Level"];

          level = Convert.ToInt32(levelGroup.Value);
          return true;
        }
      }

      level = 0;
      return false;
    }

    public string EscapeUrls(string text)
    {
      return _urlRegex.Replace(text, m =>
        {
          return "<nowiki>" + m.Value + "</nowiki>";
        });
    }

    public void Export()
    {
      // read openxml document
      using (IContentGetter contentGetter = new ZipContentGetter(_source))
      {
        Package package = new Package(contentGetter);
        _document = Document.CreateDocument(package);

        // process content and build mediawiki text
        foreach (Section section in _document.Sections)
        {
          ProcessSection(section);
        }

        // save to output stream using specified encoding
        StreamWriter writer = new StreamWriter(_output, OutputEncoding);
        writer.Write(_builder.ToString());
        writer.Flush();
      }
    }

    private void ProcessSection(Section section)
    {
      ProcessBlockContent(section.Content);
    }

    private void PushState(State state)
    {
      _state.Add(state);
    }

    private State PeekState()
    {
      return _state.Last();
    }

    private State PopState()
    {
      State state = PeekState();
      _state.RemoveAt(_state.Count - 1);

      return state;
    }

    private void ClearListState()
    {
      for (int i = _state.Count - 1; i >= 0; i--)
      {
        if (_state[i] is ListState)
          _state.RemoveAt(i);
      }
    }

    private void ProcessBlockContent(IList<DocumentContent> blockContentList)
    {
      for (int i = 0; i < blockContentList.Count; i++)
      {
        DocumentContent blockContent = blockContentList[i];

        if (blockContent is Paragraph)
        {
          Paragraph current = (Paragraph)blockContent;
          Paragraph previous = ((i > 0) ? blockContentList[i - 1] : null) as Paragraph;
          Paragraph next = ((i + 1 < blockContentList.Count) ? blockContentList[i + 1] : null) as Paragraph;

          if (previous == null || current.Properties.NumberingLevel == null)
            ClearListState();

          if (current.Properties.NumberingLevel != null && previous != null && previous.Properties.NumberingLevel != null)
          {
            if (previous.Properties.NumberingLevel.Level < current.Properties.NumberingLevel.Level)
            {
              PushState(GetListType(previous.Properties.NumberingLevel.NumberFormat));
            }
            else if (previous.Properties.NumberingLevel.Level > current.Properties.NumberingLevel.Level)
            {
              // ensure that there aren't more state levels than the current level
              while (current.Properties.NumberingLevel.Level < _state.Count)
              {
                PopState();
              }
            }
          }

          StateContext headerContext = null;
          int level;
          if (IsHeading(current.Properties.StyleId, out level))
          {
            headerContext = StateContext(new HeaderState { Level = level });
          }

          ProcessParagraph(current, previous, next);

          if (headerContext != null)
            headerContext.Dispose();
        }
        else if (blockContent is Table)
        {
          ProcessTable((Table) blockContent);
        }
        else if (blockContent is BlockStructuredTag)
        {
          BlockStructuredTag blockStructuredTag = (BlockStructuredTag) blockContent;

          ProcessBlockContent(blockStructuredTag.Content);
        }
      }
    }

    private void ProcessInlineContent(IList<InlineContent> inlineContentList)
    {
      for (int i = 0; i < inlineContentList.Count; i++)
      {
        InlineContent inlineContent = inlineContentList[i];

        if (inlineContent is Run)
        {
          Run current = (Run)inlineContent;
          InlineContent previous = ((i > 0) ? inlineContentList[i - 1] : null);
          InlineContent next = ((i + 1 < inlineContentList.Count) ? inlineContentList[i + 1] : null);

          ProcessRun(current, previous, next);
        }
        else if (inlineContent is Hyperlink)
        {
          Hyperlink hyperlink = (Hyperlink)inlineContent;

          HyperlinkState hyperlinkState = new HyperlinkState {HyperlinkUri = hyperlink.HyperlinkUri};
          using (StateContext(hyperlinkState))
          {
            ProcessInlineContent(hyperlink.Content);

            if (hyperlinkState.OpenLink)
            {
              _builder.Append("]");
              hyperlinkState.OpenLink = false;
            }
          }
        }
        else if (inlineContent is SmartTag)
        {
          SmartTag smartTag = (SmartTag)inlineContent;

          ProcessInlineContent(smartTag.Content);
        }
        else if (inlineContent is Field)
        {
          Field field = (Field) inlineContent;

          ProcessInlineContent(field.Content);
        }
      }
    }

    private void ProcessRun(Run run, InlineContent previous, InlineContent next)
    {
      Run previousRun = previous as Run;
      Run nextRun = next as Run;

      for (int i = 0; i < run.Content.Count; i++)
      {
        RunContent runContent = run.Content[i];

        if (!HasState<HeaderState>())
        {
          if (run.Properties.Italics.GetValueOrDefault() && (previousRun == null || !previousRun.Properties.Italics.GetValueOrDefault()))
            _builder.Append("''");
          if (run.Properties.Bold.GetValueOrDefault() && (previousRun == null || !previousRun.Properties.Bold.GetValueOrDefault()))
            _builder.Append("'''");
        }

        HyperlinkState hyperlinkState;
        if (HasState<HyperlinkState>(out hyperlinkState))
        {
          if (!hyperlinkState.OpenLink)
          {
            _builder.Append("[");
            _builder.Append(hyperlinkState.HyperlinkUri);
            _builder.Append(" ");
            hyperlinkState.OpenLink = true;
          }
        }

        if (runContent is Text)
        {
          Text text = (Text)runContent;

          string formattedText = text.Value.Replace("\u00A0", "&nbsp;");
          formattedText = formattedText.Replace("<", "&lt;");
          formattedText = formattedText.Replace(">", "&gt;");

          bool lineStart = (previous == null && i == 0);
          if (ShouldFormatText(formattedText, lineStart))
          {
            formattedText = "<nowiki>" + formattedText + "</nowiki>";
          }
          else
          {
            if (hyperlinkState == null)
              formattedText = EscapeUrls(formattedText);
          }

          _builder.Append(formattedText);
        }
        else if (runContent is Tab)
        {
          _builder.Append("    ");
        }
        else if (runContent is Image)
        {
          Image image = (Image)runContent;

          if (HasState<HyperlinkState>(out hyperlinkState))
          {
            if (hyperlinkState.OpenLink)
            {
              _builder.Append("]");
              hyperlinkState.OpenLink = false;
            }
          }

          int? expicitWidth = null;
          int? expicitHeight = null;

          // double check the image size specified in the document against the actual image
          // only include the sizes in mediawiki if they are different from the real image
          using (Stream bitmapStream = _document.Package.ContentGetter.GetContent(PackageUriHelper.GetDownloaderPartName(image.ImageUri)))
          using (Bitmap bitmap = new Bitmap(bitmapStream))
          {
            int documentWidth = Convert.ToInt32(Math.Round(image.Width.ToPixels(), MidpointRounding.AwayFromZero));
            int documentHeight = Convert.ToInt32(Math.Round(image.Height.ToPixels(), MidpointRounding.AwayFromZero));

            if (bitmap.Width != documentWidth)
              expicitWidth = documentWidth;
            if (bitmap.Height != documentHeight)
              expicitHeight = documentHeight;
          }

          _builder.Append("[[image:{0}".FormatWith(image.Name));

          if (expicitHeight != null)
            _builder.Append("|{0}x{1}px".FormatWith(expicitWidth, expicitHeight));
          else if (expicitWidth != null)
            _builder.Append("|{0}px".FormatWith(expicitWidth));

          Uri hyperlinkUri = null;
          if (image.HyperlinkUri != null)
            hyperlinkUri = image.HyperlinkUri;
          else if (hyperlinkState != null)
            hyperlinkUri = hyperlinkState.HyperlinkUri;
          if (hyperlinkUri != null)
            _builder.Append("|link={0}".FormatWith(hyperlinkUri.ToString()));

          if (!string.IsNullOrEmpty(image.Description))
            _builder.Append("|{0}".FormatWith(image.Description));
          _builder.Append("]]");
        }
        else if (runContent is LineBreak)
        {
          LineBreak lineBreak = (LineBreak)runContent;

          switch (lineBreak.Type)
          {
            case LineBreakType.Line:
            case LineBreakType.Page:
              _builder.AppendLine();
              _builder.AppendLine();
              break;
            case LineBreakType.Column:
            case LineBreakType.TextWrapping:
            default:
              throw new ArgumentOutOfRangeException("Type", "Unexpected line break type: " + lineBreak.Type);
          }
        }

        if (!HasState<HeaderState>())
        {
          if (run.Properties.Italics.GetValueOrDefault() && (nextRun == null || !nextRun.Properties.Italics.GetValueOrDefault()))
            _builder.Append("''");
          if (run.Properties.Bold.GetValueOrDefault() && (nextRun == null || !nextRun.Properties.Bold.GetValueOrDefault()))
            _builder.Append("'''");
        }
      }
    }

    private bool ShouldFormatText(string text, bool lineStart)
    {
      if (text.Contains("<") || text.Contains("[") || text.Contains("''"))
        return true;

      if (lineStart)
      {
        if (text.StartsWith("----") || text.StartsWith("=") || text.StartsWith("*") || text.StartsWith(";") || text.StartsWith("#"))
          return true;
      }

      return false;
    }

    private void ProcessParagraph(Paragraph paragraph, Paragraph previous, Paragraph next)
    {
      PreParagraphFormatting(paragraph);

      ProcessInlineContent(paragraph.Content);

      PostParagraphFormatting(paragraph, next);
    }

    private void PostParagraphFormatting(Paragraph paragraph, Paragraph next)
    {
      HeaderState headerState;
      if (!HasState<HeaderState>(out headerState))
      {
        if (paragraph.Properties.Alignment == HorizontalAlignment.Center)
          _builder.Append("</center>");
        else if (paragraph.Properties.Alignment == HorizontalAlignment.Right)
          _builder.Append(@"</div>");
      }
      else
      {
        _builder.Append(GetHeadingText(headerState));
      }

      if (paragraph.Properties.NumberingLevel == null || next == null || next.Properties.NumberingLevel == null)
        _builder.AppendLine();

      _builder.AppendLine();
    }

    private void PreParagraphFormatting(Paragraph paragraph)
    {
      HeaderState headerState;
      if (!HasState<HeaderState>(out headerState))
      {
        if (paragraph.Properties.NumberingLevel != null)
        {
          string listPrefix = null;

          foreach (State state in _state.Where(s => s is ListState))
          {
            listPrefix += GetListTypeChar(state.StateType);
          }

          for (int i = _state.Count; i < paragraph.Properties.NumberingLevel.Level; i++)
          {
            listPrefix += ":";
          }

          ListState currentListType = GetListType(paragraph.Properties.NumberingLevel.NumberFormat);
          listPrefix += GetListTypeChar(currentListType.StateType);

          _builder.Append(listPrefix);
          _builder.Append(" ");
        }

        if (paragraph.Properties.Alignment == HorizontalAlignment.Center)
          _builder.Append("<center>");
        else if (paragraph.Properties.Alignment == HorizontalAlignment.Right)
          _builder.Append(@"<div align=""right"">");
      }
      else
      {
        _builder.Append(GetHeadingText(headerState));
      }
    }

    private string GetHeadingText(HeaderState headerState)
    {
      int level = Math.Min(headerState.Level, 6);
      return new string('=', level);
    }

    private bool HasState<T>() where T : State
    {
      T state;
      return HasState(out state);
    }

    private bool HasState<T>(out T state) where T : State
    {
      for (int i = _state.Count - 1; i >= 0; i--)
      {
        if (_state[i] is T)
        {
          state = (T)_state[i];
          return true;
        }
      }

      state = null;
      return false;
    }

    private char GetListTypeChar(StateType listType)
    {
      switch (listType)
      {
        case StateType.OrderedList:
          return '#';
        case StateType.UnorderedList:
          return '*';
        default:
          throw new ArgumentOutOfRangeException("listType");
      }
    }

    private ListState GetListType(NumberFormat numberFormat)
    {
      switch (numberFormat)
      {
        case NumberFormat.Decimal:
        case NumberFormat.CardinalText:
        case NumberFormat.LowerLetter:
        case NumberFormat.LowerRoman:
        case NumberFormat.Ordinal:
        case NumberFormat.UpperLetter:
        case NumberFormat.UpperRoman:
          return ListState.OrderList;
        case NumberFormat.Bullet:
          return ListState.UnorderedList;
        default:
          throw new ArgumentOutOfRangeException("numberFormat");
      }
    }

    private void ProcessTable(Table table)
    {
      _builder.AppendLine(@"{| class=""prettytable""");

      foreach (TableRow row in table.Rows)
      {
        _builder.AppendLine("|-");

        foreach (TableCell cell in row.Cells)
        {
          if (cell.Properties.GridSpan > 1)
            _builder.AppendLine(@"| colspan=""{0}"" |".FormatWith(cell.Properties.GridSpan));
          else
            _builder.AppendLine("|");

          ProcessBlockContent(cell.Content);
        }
      }

      _builder.AppendLine("|}");
    }
  }
}
